package queenProblem;

import GUI.StartWindow;

import java.awt.Point;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.SwingWorker;


/**
 * The solver for the QueenProblem.
 * 
 * @author Jakob Karolus, Kevin Munk
 * @version 1.0
 * 
 */
public class QueenProblemSolver extends SwingWorker<Boolean, int[]> {

	/**
	 * The play field on which the queens are set. 
	 */
	private PlayField playField;
	/**
	 * The delay between calls to solve().
	 */
	private int delay;
	/**
	 * A reference to the GUI.
	 */
	private StartWindow startWindow;
	
	/**
	 * Initiates the Solver with the playFieldSize and initiates the playField.
	 * 
	 * @param playField
	 *            the playField to work on
	 * @param delay
	 *            the delay between an solver move (should not be under 100 ms)
	 */
	public QueenProblemSolver(boolean[][] playField, int delay) {
		this(playField, delay, null);
	}
	
	/**
	 * Initiates the Solver with the playFieldSize and initiates the playField. Additionally,
	 * it initializes the delay and a reference to the GUI.
	 * @param playField The play field on which the queens are set.
	 * @param delay The delay between calls to setQueen() and removeQueen().
	 * @param startWindow A reference to the GUI.
	 */
	public QueenProblemSolver(boolean[][] playField, int delay, StartWindow startWindow) {
		if(! isValid(playField))
			throw new RuntimeException("Play field not valid");
		
		this.playField = new PlayField(playField);
		this.startWindow = startWindow;
		// See javadoc above
		if (delay < 100){
			if(startWindow != null) 
				startWindow.displayDelayError();
			else // we have no GUI
				System.err.println("Delay should not be below 100ms");
			// Correct to 100ms
			this.delay = 100;
		} else {
			this.delay = delay;
		}
		
		if(startWindow != null) {
			startWindow.addPropertyChangeListener(new PropertyChangeListener(){
				@Override
				public void propertyChange(PropertyChangeEvent e) {
					if (e.getPropertyName().equals("solve")){
						try {
							execute();
						} catch (Exception e1) {
							e1.printStackTrace();
						}
					}
					
				}
			});
		}

	}

	private boolean isValid(boolean[][] playField2) {
		if(playField2 == null)
			return false;
		if(playField2.length < 1)
			return false;
		
		final int Length = playField2.length;
		for(int row=0; row <playField2.length; ++row) {
			if(playField2[row].length != Length)
				return false;
		}
		
		return true;
	}

	/**
	 * Spawns a new thread in which the statements of doInBackground() are executed.
	 * @return The computed result
	 * @throws Thrown if no result can be computed
	 */
	@Override
	public Boolean doInBackground() throws Exception {
		boolean solved = solve();
		if(startWindow != null)
			startWindow.displaySolveResult(solved);
		return solved;
	}
	
	

	/**
	 * starts the solve method from the upper left corner.
	 * 
	 * @return true if the playField could be solved; otherwise false
	 */
	public boolean solve() {
		int currentRow = 0;
		while (currentRow < getPlayFieldSize()){
			if (queenInRow(currentRow)){
				currentRow ++;
				continue;
			} else {
				if (currentRow % 2 == 0){
					for (int i = 0; i < getPlayFieldSize(); i++){
						if(isQueenSettable(currentRow, i)){
							setQueen(currentRow, i);
							
							try {
								Thread.sleep(delay);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							
							if (solve()){
								return true;
							} else {
								removeQueen(currentRow);
							}
						}
					}
					try {
						Thread.sleep(delay);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					return false;
				} else {
					for (int i = getPlayFieldSize()-1; i > -1; i--){
						if(isQueenSettable(currentRow, i)){
							setQueen(currentRow, i);
							
							try {
								Thread.sleep(delay);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							
							if (solve()){
								return true;
							} else {
								removeQueen(currentRow);
							}
						}
					}
					try {
						Thread.sleep(delay);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					return false;
				}
			}
		}
		return true;
	}
	
	
	/**
	 * Removes a queen from a row. The column is found automatically.
	 * @param cRow Row from which queen should be removed.
	 */
	private void removeQueen(int cRow) {
		// iterate over columns
		for(int c=0; c<playField.getPlayFieldSize(); ++c) {
			if(playField.getPlayField()[cRow][c]) {
				// Notify GUI
				if(startWindow != null)
					startWindow.removeQueen(cRow, c);
				// Change data model
				boolean[][] pf = playField.getPlayField();
				// Field now empty again
				pf[cRow][c] = false;
				// PlayField doesn't offer any setters to a new PlayField is created.
				playField = new PlayField(pf);
				
				break;
			}
		}
		 
	}
	/**
	 * Checks if a queen is in the specified row.
	 * @param cRow The given row
	 * @return True if queen is in cRow; false otherwise
	 */
	private boolean queenInRow(int cRow) {
		// iterate over column
		for(int c=0; c<playField.getPlayFieldSize(); ++c)
			if(playField.getPlayField()[cRow][c]) return true; // a true (=queen) was encountered
		return false;
	}

	/**
	 * Checks if a queen can be set at a certain position on a certain play field.
	 * (Declaring it static makes it available for use in other classes.
	 * @param row y-coordinate
	 * @param column x-coordinate
	 * @param playField play field in which to check
	 * @return True if queen can be set; false otherwise
	 */
	public static boolean isQueenSettable(int row, int column, PlayField playField)
	{
		final int Length = playField.getPlayFieldSize();
		 final boolean[][] pField = playField.getPlayField();
		 
		 // queen in same row?
		 for(int r=0; r<Length; ++r) {
			 if(pField[r][column]) return false;
		 }
		 // queen in same column?
		 for(int c=0; c<Length; ++c) {
			 if(pField[row][c]) return false;
		 }
		 // queen opposed diagonally?
		 // iterate from startPoint to endPoint and check for true (=queen)
		 int delta1 = Math.min(row, column), // space to the top and left
		 	 delta2 = Math.min(Length-row-1, Length-column-1), // space to the bottom and right
		 	 delta3 = Math.min(row, Length-column-1), // space to top and right
		 	 delta4 = Math.min(Length-row-1, column); // space to bottom and left
		 // start- and end points
		 Point upperLeftSP = new Point(row-delta1, column-delta1),
		 	   upperRightSP = new Point( row-delta3, column+delta3),
		 	   lowerLeftEP = new Point(row+delta4, column-delta4),
		 	   lowerRightEP = new Point(row+delta2, column+delta2);
		 // diagonal from upper left to lower right corner
		 for(int i=0; i<lowerRightEP.x-upperLeftSP.x+1; ++i)
		 {
			 if(pField[upperLeftSP.x+i][upperLeftSP.y+i])
				 return false;
		 }
		 // diagonal from upper right to lower left corner
		 for(int i=0; i<lowerLeftEP.x-upperRightSP.x+1; ++i)
		 {
			 if(pField[upperRightSP.x+i][upperRightSP.y-i])
				 return false;
		 }
		 
		 
		 return true; 
	}
	
	/**
	 * This method must be called BEFORE a queen is set. 
	 * (Uses the static version.)
	 * @param int row shows the position on row
	 * @param int column shows  the position on column
	 * @return true if  we can set a queen on this position
	 */
	 public boolean isQueenSettable(int row, int column){
		 return isQueenSettable(row, column, playField);
	 }

	
	
	/**
	 * Can ONLY be used for RETRIEVAL NOT SETTING.
	 * @return the play field
	 */
	public boolean[][] getPlayField() {
		return this.playField.getPlayField();
	}


	/**
	 * Sets a queen at the specified position
	 * 
	 * @param row y coordinate
	 * @param column x coordinate
	 */
	public void setQueen(int row, int column) {
		
		// Notify GUI
		if(startWindow != null)
			startWindow.setQueen(row, column);
		// Change the data model
		boolean[][] pf = getPlayField();
		pf[row][column] = true;
		// No setters in PlayField -> Create new PlayField
		playField = new PlayField(pf);
		
	}
	
	/**
	 * Returns a printable version of a QueenProblemSolver object.
	 * @return QueenProblemSolver as string
	 */
	@Override
	public String toString(){
		String hu = "";
		
		// iterate over all fields
		for(int i =0;i<getPlayField().length;i++){
			for(int j = 0;j<getPlayField().length;j++){
			
				hu = hu + " " + getPlayField()[i][j];
			}
			hu = hu + "\n";
		}
		return hu;
	}
	

	/**
	 * Returns the size of the quadratic play field.
	 * @return the size of the play field
	 */
	public int getPlayFieldSize() {
		return this.playField.getPlayFieldSize();
	}
}

